package de.hu.wbi.histoner;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.google.code.regexp.NamedMatcher;
import com.google.code.regexp.NamedPattern;

import de.hu.wbi.histoner.HistoneModification.Residue;

/**
Copyright 2011,2012 Philippe Thomas
This file is part of histoner.

HistoNer is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.

HistoNer  is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with HistoNer .  If not, see <http://www.gnu.org/licenses/>.
*/

/**
 * Named entity recognition for histone modifications 
 * @author philippe
 * @version 1.0
 */
public class HistoNER {
	
	final private String prefix="(?<a>^|[>\\s\\(\\)\\[\\'\"/,\\-])";		//All regular expressions get a prefix and postfix
	final private String suffix="(?<b>?=([<\\.,\\s\\)\\(\\]\\'\":;\\-?!/]|$))";
		
	//A histone modification consists of four different subparts: (1. Histone Number; 2. amino acid; 3. location; 4. modification)
	final private String histone="1X?|2X?|2AX?|2BX?|3X?|4X?";		//Histone number
//	final private String aa ="lysine|lys|arginine|arg|threonine|thr|serine|ser|glutamine|glutamatic acid|glu|K|R|S|T|E"; //Amino acids
	final private String aa =HistoneModification.getResidueRegex();
	final private String aaloc="[1-9][0-9]*";	//location
	final private String modification=HistoneModification.getModificationRegex();//Modification type
	
	private Set<NamedPattern> patterns;
	final private Map<String, Residue> residueMapping  = HistoneModification.residueMapping();
	final private Map<Pattern, String> modificationMapping = HistoneModification.getModificationMapping();
	
	/**
	 * Constructor with supplied regexFile
	 */
	public HistoNER (String regexFile){
		readRegexFromFile(regexFile, Integer.MAX_VALUE);
	}
	
	/**
	 * Core of HistoNer:
	 * Searches and normalizes histone modifciations to Brno-nomenclature
	 * @param text Text where NER is applied on
	 * @return Set of histone modifications found in this article
	 */
	public Set<HistoneModification> extractHistoneModifications (String text){
		Pattern startPattern = Pattern.compile("^[\\p{Punct}\\p{Space}]");
//		Pattern endPattern = Pattern.compile("[\\p{Punct}\\p{Space}]$");
	
		Set<HistoneModification> result = new TreeSet<HistoneModification>();
		
		for(NamedPattern pattern:patterns){ // Iterate over all precompiled pattern and try to match them 
			NamedMatcher matcher = pattern.matcher(text);
			
			while(matcher.find()){
				
				String patternId = pattern.getId();
				
				String histone= matcher.group("hnumber");
				String modification = getModString(matcher.group("modification"));// modificationMapping.get(matcher.group("modification").toLowerCase());
				Residue r = residueMapping .get(matcher.group("aa").toUpperCase());				
				int loc = Integer.parseInt(matcher.group("aanumber"));
				
				//Simple postprocessing step, making the resulting string prettier, by removing unwanted hits such as '-'
				int startPos = matcher.start();
				int endPos = matcher.end();
				String entityText = text.substring(startPos, endPos);

				Matcher m = startPattern.matcher(entityText);
				if(m.find()){
					startPos++;
				}										
				entityText =  text.substring(startPos, endPos);
	
				//Duplicate histone modification for phrases describing several modifications (e.g. di-tri methylation -> me2, me3)
				if(modification.contains("|")){
					String [] array = modification.split("\\|");
					for(String mod:array){
						HistoneModification histoneMod = new HistoneModification(histone, mod, r, loc, startPos, endPos, entityText);
						histoneMod.setPatternId(patternId);
						result.add(histoneMod);
					}
				}
				else{	
					HistoneModification histoneMod = new HistoneModification(histone, modification,  r, loc, startPos, endPos, entityText);					
					histoneMod.setPatternId(patternId);
					result.add(histoneMod);
				}
			}
		}
		return removeOverlaps(result);
	}
	
	/**
	 * Removes all duplicate histone modifications which describe the same histonemodification
	 * @param mods All modifications found in one article
	 * @return set Unique non overlapping modifications found in one article
	 */
	private static Set<HistoneModification> removeOverlaps(Set<HistoneModification> mods){
		List<HistoneModification> result = new ArrayList<HistoneModification>();
		
		for(HistoneModification mod:mods){
			
			boolean add=true;
			for(int i=0; i<result.size();i++){
				
				HistoneModification other = result.get(i);
				if(mod.overlaps(other) && other.toBrnoString().equals(mod.toBrnoString())){
					add=false;
					if((mod.to-mod.from) > (other.to-other.from)){//New element is more representative
						result.remove(i);
						result.add(mod);	
					}	
				}
			}
			if(add)
				result.add(mod);
		}
		return new TreeSet<HistoneModification>(result);
	}
	
	/**
	 * Normalizes a modification string (E.g. Di-methylation is normalized to me2)
	 * @param modification Modification term
	 * @return	Normalized modification term
	 */
	private String getModString (String modification){
	
		List<String> hits = new ArrayList<String>();
		for(Pattern pattern:modificationMapping.keySet()){
			Matcher matcher = pattern.matcher(modification);
			
			if (matcher.matches()){
				hits.add(modificationMapping.get(pattern));			
			}		
		}
		
		if(hits.size() == 1){
			return hits.get(0);
		}
		if(hits.size() > 1){
			System.err.println("Too many hits for '"  +modification +"'");
			return hits.get(0);
		}

		System.err.println("Should not happen for '"  +modification +"'");
		System.exit(1);
		return "error";
	}
	

	/**
	 * Reads named regular expressions from a file and adds them into a Set of Pattern
	 * Placeholder for entities (<aa>, <hnumber>, etc.) are replaced by a valid regular expression
	 * @param file File consisting of regular expressions
	 */
	private void readRegexFromFile(String file, int howMany){
		patterns = new HashSet<NamedPattern>();
		
		BufferedReader br = null;
		try {
			br = new BufferedReader(new InputStreamReader(new FileInputStream(file),"UTF8"));
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}

		String line= null;
		int nLines = 0;
		List<String> strings = new ArrayList<String>();

		try {
			while (( line = br.readLine()) != null ){		
				nLines++;
							
				if( !line.startsWith("#")){ //Commented lines are not used to generate a pattern
					
					if(! (line.contains("<hnumber>") && line.contains("<aa>") && line.contains("<aanumber>") && line.contains("<mod>")) ){
						System.out.println("Ignoring incomplete pattern '" +line +"'");
						continue;
					}						
					
					String []array  = line.split("\t");
					line= array[0];
					line = line.trim();
					
					line = line.replaceAll("<hnumber>", "(?<hnumber>^[0-9]" +histone +")");  //No preceeding number for histones; covers regexes like  <hnumber><aa><aanumber>\) <mod> 2
					line = line.replaceAll("<aa>", "(?<aa>" +aa +")");
					line = line.replaceAll("<aanumber>", "(?<aanumber>" +aaloc +")");
					line = line.replaceAll("<mod>", "(?<modification>" +modification +")");					
					
					line = line.replaceAll("<jaa>", "(?<jaa>" +aa +")");
					line = line.replaceAll("<jmod>", "(?<jmodification>" +modification  +")");
					line = line.replaceAll("<jnumber>", "(?<jnumber>^[0]" +histone +"|" +aaloc +")");
							
					OptionalRegexOptimizer oro = new OptionalRegexOptimizer();
					
					if(!strings.contains(line)){
						
						if(array.length >= 3)
							patterns.add(NamedPattern.compile(oro.optimize(prefix+line+suffix), Pattern.CASE_INSENSITIVE, array[2]));
						else
							patterns.add(NamedPattern.compile(oro.optimize(prefix+line+suffix), Pattern.CASE_INSENSITIVE, "ID=" +nLines));
					}
						
					strings.add(line);
				}
			}	
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		System.err.println("Successfully loaded " +patterns.size() +" unique pattern" +" from " +nLines +" lines" );
	}
	
}
